/*
 * Copyright 2019 Distributed Systems Group
 *
 * <p>Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package simblock.node;

import static simblock.settings.SimulationConfiguration.BLOCK_SIZE;
import static simblock.simulator.Main.OUT_JSON_FILE;
import static simblock.simulator.Network.getBandwidth;
import static simblock.simulator.Simulator.arriveBlock;
import static simblock.simulator.Timer.getCurrentTime;
import static simblock.simulator.Timer.putTask;
import static simblock.simulator.Timer.removeTask;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import simblock.block.Block;
import simblock.node.consensus.AbstractConsensusAlgo;
import simblock.node.routing.AbstractRoutingTable;
import simblock.task.AbstractMessageTask;
import simblock.task.AbstractMintingTask;
import simblock.task.BlockMessageTask;
import simblock.task.InvMessageTask;
import simblock.task.RecMessageTask;

/**
 * A class representing a node in the network.
 */
public class Node {
  /**
   * Unique node ID.
   */
  private final int nodeID;

  /**
   * Region assigned to the node.
   */
  private final int region;

  /**
   * Mining power assigned to the node.
   */
  private final long miningPower;

  /**
   * A nodes routing table.
   */
  private AbstractRoutingTable routingTable;

  /**
   * The consensus algorithm used by the node.
   */
  private AbstractConsensusAlgo consensusAlgo;

  /**
   * The current block.
   */
  private Block block;

  /**
   * Orphaned blocks known to node.
   */
  private final Set<Block> orphans = new HashSet<>();

  /**
   * The current minting task
   */
  private AbstractMintingTask mintingTask = null;

  /**
   * In the process of sending blocks.
   */
  // TODO verify
  private boolean sendingBlock = false;

  //TODO
  private final ArrayList<RecMessageTask> messageQue = new ArrayList<>();
  // TODO
  private final Set<Block> downloadingBlocks = new HashSet<>();

  /**
   * Processing time of tasks expressed in milliseconds.
   */
  private final long processingTime = 2;

  /**
   * Instantiates a new Node.
   *
   * @param nodeID            the node id
   * @param numConnection     the number of connections a node can have
   * @param region            the region
   * @param miningPower       the mining power
   * @param routingTableName  the routing table name
   * @param consensusAlgoName the consensus algorithm name
   */
  public Node(
      int nodeID, int numConnection, int region, long miningPower, String routingTableName,
      String consensusAlgoName
  ) {
    this.nodeID = nodeID;
    this.region = region;
    this.miningPower = miningPower;
    try {
      this.routingTable = (AbstractRoutingTable) Class.forName(routingTableName).getConstructor(
          Node.class).newInstance(this);
      this.consensusAlgo = (AbstractConsensusAlgo) Class.forName(consensusAlgoName).getConstructor(
          Node.class).newInstance(this);
      this.setNumConnection(numConnection);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Gets the node id.
   *
   * @return the node id
   */
  public int getNodeID() {
    return this.nodeID;
  }

  /**
   * Gets the region ID assigned to a node.
   *
   * @return the region
   */
  public int getRegion() {
    return this.region;
  }

  /**
   * Gets mining power.
   *
   * @return the mining power
   */
  public long getMiningPower() {
    return this.miningPower;
  }

  /**
   * Gets the consensus algorithm.
   *
   * @return the consensus algorithm. See {@link AbstractConsensusAlgo}
   */
  @SuppressWarnings("unused")
  public AbstractConsensusAlgo getConsensusAlgo() {
    return this.consensusAlgo;
  }

  /**
   * Gets routing table.
   *
   * @return the routing table
   */
  public AbstractRoutingTable getRoutingTable() {
    return this.routingTable;
  }

  /**
   * Gets the current block.
   *
   * @return the block
   */
  public Block getBlock() {
    return this.block;
  }

  /**
   * Gets all orphans known to node.
   *
   * @return the orphans
   */
  public Set<Block> getOrphans() {
    return this.orphans;
  }

  /**
   * Gets the number of connections a node can have.
   *
   * @return the number of connection
   */
  @SuppressWarnings("unused")
  public int getNumConnection() {
    return this.routingTable.getNumConnection();
  }

  /**
   * Sets the number of connections a node can have.
   *
   * @param numConnection the n connection
   */
  public void setNumConnection(int numConnection) {
    this.routingTable.setNumConnection(numConnection);
  }

  /**
   * Gets the nodes neighbors.
   *
   * @return the neighbors
   */
  public ArrayList<Node> getNeighbors() {
    return this.routingTable.getNeighbors();
  }

  /**
   * Adds the node as a neighbor.
   *
   * @param node the node to be added as a neighbor
   * @return the success state of the operation
   */
  @SuppressWarnings("UnusedReturnValue")
  public boolean addNeighbor(Node node) {
    return this.routingTable.addNeighbor(node);
  }

  /**
   * Removes the neighbor form the node.
   *
   * @param node the node to be removed as a neighbor
   * @return the success state of the operation
   */
  @SuppressWarnings("unused")
  public boolean removeNeighbor(Node node) {
    return this.routingTable.removeNeighbor(node);
  }

  /**
   * Initializes the routing table.
   */
  public void joinNetwork() {
    this.routingTable.initTable();
  }

  /**
   * Mint the genesis block.
   */
  public void genesisBlock() {
    Block genesis = this.consensusAlgo.genesisBlock();
    this.receiveBlock(genesis);
  }

  /**
   * Adds a new block to the to chain. If node was minting that task instance is abandoned, and
   * the new block arrival is handled.
   *
   * @param newBlock the new block
   */
  public void addToChain(Block newBlock) {
    // If the node has been minting
    if (this.mintingTask != null) {
      //Timer.removeTask
      removeTask(this.mintingTask);
      this.mintingTask = null;
    }
    // Update the current block
    this.block = newBlock;
    printAddBlock(newBlock);
    // Observe and handle new block arrival
    // Simulator.arriveBlock 通知模拟器到达了区块
    arriveBlock(newBlock, this);
  }

  /**
   * Logs the provided block to the logfile.
   *
   * @param newBlock the block to be logged
   */
  private void printAddBlock(Block newBlock) {
    OUT_JSON_FILE.print("{");
    OUT_JSON_FILE.print("\"kind\":\"add-block\",");
    OUT_JSON_FILE.print("\"content\":{");
    OUT_JSON_FILE.print("\"timestamp\":" + getCurrentTime() + ",");
    OUT_JSON_FILE.print("\"node-id\":" + this.getNodeID() + ",");
    OUT_JSON_FILE.print("\"block-id\":" + newBlock.getId());
    OUT_JSON_FILE.print("}");
    OUT_JSON_FILE.print("},");
    OUT_JSON_FILE.flush();
  }

  /**
   * Add orphans.
   *
   * @param orphanBlock the orphan block
   * @param validBlock  the valid block
   */
  //TODO check this out later
  public void addOrphans(Block orphanBlock, Block validBlock) {
    if (orphanBlock != validBlock) {
      this.orphans.add(orphanBlock);
      this.orphans.remove(validBlock);
      if (validBlock == null || orphanBlock.getHeight() > validBlock.getHeight()) {
        this.addOrphans(orphanBlock.getParent(), validBlock);
      } else if (orphanBlock.getHeight() == validBlock.getHeight()) {
        this.addOrphans(orphanBlock.getParent(), validBlock.getParent());
      } else {
        this.addOrphans(orphanBlock, validBlock.getParent());
      }
    }
  }

  /**
   * Generates a new minting task and registers it
   */
  public void minting() {
    AbstractMintingTask task = this.consensusAlgo.minting();
    this.mintingTask = task;
    if (task != null) {
      putTask(task);
    }
  }

  /**
   * Send inv.
   *
   * @param block the block
   */
  public void sendInv(Block block) {
    //发送给所有邻居节点
    for (Node to : this.routingTable.getNeighbors()) {
      AbstractMessageTask task = new InvMessageTask(this, to, block);
      // Timer.putTask
      putTask(task);
    }
  }

  /**
   * Receive block.
   *
   * @param block the block
   */
  public void receiveBlock(Block block) {
    if (this.consensusAlgo.isReceivedBlockValid(block, this.block)) {
      //如果共识协议判断该区块是正确的
      if (this.block != null && !this.block.isOnSameChainAs(block)) {
        // 如果主链的最新区块不是空的 ，并且给的区块不在本条主链上， 那么加入到孤块中
        // If orphan mark orphan
        this.addOrphans(this.block, block);
      }
      // FIXME ???? 注释else，但是实际没有,只是为了打印出来，前面做了一个标记
      // Else add to canonical chain
      // 添加到达区块的同时，废弃本节点的挖矿任务，同时通知模拟器区块的到达
      this.addToChain(block);
      // Generates a new minting task
      // 新建挖矿任务
      this.minting();

      // Advertise received block
      // 广播给其他节点区块的到达
      // 此处可以优化一点点（不要再发给我了）
      this.sendInv(block);
    } else if (!this.orphans.contains(block) && !block.isOnSameChainAs(this.block)) {
      // 如果共识协议不正确，如果本节点的孤块列表中没有包含该区块 ， 并且该区块也不在主链上
      // 加入到孤块中，直接通知模拟器区块的到达
      // TODO better understand - what if orphan is not valid?
      // If the block was not valid but was an unknown orphan and is not on the same chain as the
      // current block
      // 会根据孤块的父区块 和 本区块一直追溯，找到交叉点
      this.addOrphans(block, this.block);
      arriveBlock(block, this);
    }
  }

  /**
   * Receive message.
   *
   *      BlockMessage -->
   *       \/------------|
   * InvMessage ----> RecMessage
   *              \-> RecMessage
   *       /\------------|
   *     BlockMessage -->
   * @param message the message
   */
  public void receiveMessage(AbstractMessageTask message) {
    Node from = message.getFrom();

    if (message instanceof InvMessageTask) {
      // 如果是广播消息
      Block block = ((InvMessageTask) message).getBlock();
      if (!this.orphans.contains(block) && !this.downloadingBlocks.contains(block)) {
        // 如果孤块列表不包含 并且 下载的区块也不包含
        if (this.consensusAlgo.isReceivedBlockValid(block, this.block)) {
          // 如果接收的区块合法
          AbstractMessageTask task = new RecMessageTask(this, from, block);
          // 发送区块已接收消息
          putTask(task);
          // 添加到正在下载的区块列表中
          downloadingBlocks.add(block);
        } else if (!block.isOnSameChainAs(this.block)) {
          // 如果区块不是在主链上，添加到正在下载的列表，表示是一个孤块
          // get new orphan block
          AbstractMessageTask task = new RecMessageTask(this, from, block);
          putTask(task);
          downloadingBlocks.add(block);
        }
      }
    }

    if (message instanceof RecMessageTask) {
      // 如果是 接收到区块消息
      // sending block 是一个同步位？？
      this.messageQue.add((RecMessageTask) message);
      if (!sendingBlock) {
        // 这里如果RecMessage消息累积过多，可能会出现问题
        // 根据区块接收消息，新建一个区块发送消息 BlockMessage (包含延迟的)
        this.sendNextBlockMessage();
      }
    }

    if (message instanceof BlockMessageTask) {
      // 接收到区块消息，从正在下载列表中移除，同时该节点接收到区块
      Block block = ((BlockMessageTask) message).getBlock();
      downloadingBlocks.remove(block);
      // 节点接收到区块后会再次开始挖矿和广播
      this.receiveBlock(block);
    }
  }

  /**
   * Send next block message.
   */
  // send a block to the sender of the next queued recMessage
  // send 之后，将队列中的消息移除
  public void sendNextBlockMessage() {
    if (this.messageQue.size() > 0) {
      sendingBlock = true;

      Node to = this.messageQue.get(0).getFrom();
      Block block = this.messageQue.get(0).getBlock();
      this.messageQue.remove(0);
      long bandwidth = getBandwidth(this.getRegion(), to.getRegion());

      // Convert bytes to bits and divide by the bandwidth expressed as bit per millisecond, add
      // processing time.
      long delay = BLOCK_SIZE * 8 / (bandwidth / 1000) + processingTime;

      //
      BlockMessageTask messageTask = new BlockMessageTask(this, to, block, delay);

      putTask(messageTask);
    } else {
      sendingBlock = false;
    }
  }
}
